/*
 * @(#)Resource.java	1.3 02/08/21
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 */

package com.sun.media.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.media.Format;
import javax.media.format.AudioFormat;
import javax.media.format.VideoFormat;

import com.ms.security.PermissionID;
import com.ms.security.PolicyEngine;
import com.sun.media.JMFSecurity;
import com.sun.media.JMFSecurityManager;

/**
 * Class where objects can be registered with a unique key (string).
 * The Resource tries to find a ".jmf-resource" file in user.home.
 * If it does, then it reads all the keys and corresponding objects.
 * The resource file is generated and maintained solely by the MediaEngine.
 * The structure of the serialized file is:
 *     Number of items in the table: integer
 *     Version number              : integer
 *     Key for item 1              : UTF
 *     Value of item 1             : Object
 *     Key for item 2              : UTF
 *     Value of item 2             : Object
 *     and so on......
 */
public class Resource {

    // Hashtable that stores all the properties.
    private static Hashtable hash = null;

    // Name of the properties file, including path.
    private static String filename = null;

    // Version number of the serialized file format. Will need
    // to be incremented if the format changes so that an old
    // implementation will expect errors with a new format.
    private static final int versionNumber = 200;
    private static boolean securityPrivelege=false;
    private static JMFSecurity jmfSecurity = null;
    private static Method m[] = new Method[1];
    private static Class cl[] = new Class[1];
    private static Object args[][] = new Object[1][0];
    private static final String USERHOME = "user.home";
    private static String userhome = null;

    /**
     * Static code block to read in the resource file and initialize
     * the hash table.
     */
    static {
	hash = new Hashtable();

	try {
	    jmfSecurity = JMFSecurityManager.getJMFSecurity();
	    securityPrivelege = true;
	} catch (SecurityException e) {
	}

	if ( /* securityPrivelege && */ (jmfSecurity != null) ) {
	    String permission = null;
	    try {
		if (jmfSecurity.getName().startsWith("jmf-security")) {
		    permission = "read property";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.READ_PROPERTY);
		    m[0].invoke(cl[0], args[0]);


		    permission = "read file";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.READ_FILE);
		    m[0].invoke(cl[0], args[0]);
		} else if (jmfSecurity.getName().startsWith("internet")) {
 		    PolicyEngine.checkPermission(PermissionID.PROPERTY);
 		    PolicyEngine.checkPermission(PermissionID.FILEIO);
		    PolicyEngine.assertPermission(PermissionID.PROPERTY);
		    PolicyEngine.assertPermission(PermissionID.FILEIO);
		}
	    } catch (Throwable e) {
		if (JMFSecurityManager.DEBUG) {
		    System.err.println("Resource: Unable to get " + permission +
				       " privilege  " + e.getMessage());
		}
		securityPrivelege = false;
	    }
	}

	if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {
	    try {
		Constructor cons = jdk12PropertyAction.cons;
		userhome = (String) jdk12.doPrivM.invoke(
				     jdk12.ac,
				     new Object[] {
 					  cons.newInstance(
 					   new Object[] {
                                               USERHOME,
                                           })});


	    } catch (Throwable e) {
		securityPrivelege = false;
	    }
	} else {
	    try {
		if (securityPrivelege)
		    userhome = System.getProperty(USERHOME);
	    } catch (Exception e) {
		userhome = null;
		securityPrivelege = false;
	    }
	}

	if (userhome == null) {
	    securityPrivelege = false;
	}
	InputStream is = null;

        if (securityPrivelege) {
	    is = findResourceFile();
          if (is == null) {
            securityPrivelege=false; // there is no access to jmf.properties
          }
        }

	if (!readResource(is)) {
	    hash = new Hashtable();
	}
    }


    public static final synchronized void reset() {
	hash = new Hashtable();
    }


    /**
     * Add or modify a property. The key and the value should be non-null.
     * Returns false if it couldn't add/modify the property.
     */
    public static final synchronized boolean set(String key, Object value) {
	if (key != null && value != null) {
	    if (jmfSecurity != null && key.indexOf("secure.") == 0)
		return false;
	    hash.put(key, value);
	    return true;
	} else
	    return false;
    }

    /**
     * Returns the value corresponding to the specified key. Returns null
     * if no such property is found.
     */
    public static final synchronized Object get(String key) {
	if (key != null)
	    return hash.get(key);
	else
	    return null;
    }

    /**
     * Removes a property from the hashtable. Returns false
     * if the property was not found.
     */
    public static final synchronized boolean remove(String key) {
	if (key != null) {
	    if (hash.containsKey(key)) {
		hash.remove(key);
		return true;
	    }
	}

	return false;
    }

    /**
     * Removes an entire set of properties with the keys starting with
     * the value "keyStart".
     */
    public static final synchronized void removeGroup(String keyStart) {
	Vector keys = new Vector();
	if (keyStart != null) {
	    Enumeration e = hash.keys();
	    while (e.hasMoreElements()) {
		String key = (String) e.nextElement();
		if (key.startsWith(keyStart))
		    keys.addElement(key);
	    }
	}

	for (int i = 0; i < keys.size(); i++) {
	    hash.remove(keys.elementAt(i));
	}
    }
    
    /**
     * Writes all the properties in the hashtable to the
     * jmf.properties file. If the file is non-existent or the writing
     * failed for some reason, it throws an IOException.
     */
    public static final synchronized boolean commit()
    throws IOException {
	if (filename == null)
	    throw new IOException("Can't find resource file");

	FileOutputStream fos = new FileOutputStream(filename);
	ObjectOutputStream oos = new ObjectOutputStream(fos);

	int tableSize = hash.size();
	oos.writeInt(tableSize);
	oos.writeInt(versionNumber);
	for (Enumeration e = hash.keys(); e.hasMoreElements() ;) {
	    String key = (String) e.nextElement();
	    Object value = hash.get(key);

	    oos.writeUTF(key);			      // write key as UTF chars.

            /*
	    ByteArrayOutputStream baos = new ByteArrayOutputStream();
	    ObjectOutputStream tempOOS = new ObjectOutputStream(baos);
	    tempOOS.writeObject(value);
	    tempOOS.flush();
	    byte [] serObject = baos.toByteArray();
	    oos.writeObject(serObject);
	    tempOOS.close();
            */

            oos.writeObject(value);
            oos.flush();
	}
	oos.close();
	return true;
    }

    /**
     * Remove the permanent registry.
     */
    public static final synchronized void destroy() {
	if (filename == null)
	    return;
	try {
	    File file = new File(filename);
	    file.delete();
	} catch (Throwable t) {
	    filename = null;
	}
    }

   private static final synchronized InputStream findResourceFile() {

	String dir;
	String strJMF = ".jmf-resource";
	File file = null;
	InputStream ris = null;

	if (userhome == null)
	    return null;

	try {
	    filename = userhome + File.separator + strJMF;
	    //System.out.println("Resource: file name is " + filename);
	    file = new File(filename);
	    ris = getResourceStream(file);
	} catch (Throwable t) {
	    filename = null;
	    return null;
	}

	return ris;
    }


    private static final FileInputStream getResourceStream(File file) throws IOException {
	try {

	    if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {
		Constructor cons = jdk12ReadFileAction.cons;
		return (FileInputStream) jdk12.doPrivM.invoke(
				     jdk12.ac,
				     new Object[] {
 					  cons.newInstance(
 					   new Object[] {
                                               file.getPath()
                                           })});
	    
	    } else {
		if (!file.exists()) {
		    //System.out.println("file doesnt exist");
		    return null;
		} else {
		    return new FileInputStream(file.getPath());
		}
	    }
	} catch (Throwable t) {
	    return null;
	}
    }


   private static final synchronized boolean readResource(InputStream ris) {

	if (ris == null)
	    return false;

    	try {

	    // Inner class with skipHeader so that the protected method
	    // readStreamHeader can be called.
	    ObjectInputStream ois = new ObjectInputStream(ris);
	    
	    int tableSize = ois.readInt();
	    int version = ois.readInt();
	    if (version > 200) {
		System.err.println("Version number mismatch.\nThere could be" +
				   " errors in reading the resource");
	    }
	    hash = new Hashtable();
	    for (int i = 0; i < tableSize; i++) {
		String key = ois.readUTF();
		boolean failed = false;
		byte [] serObject;
		try {
                    /*
		    serObject = (byte[])ois.readObject();
		    ByteArrayInputStream bais = new ByteArrayInputStream(serObject);
		    ObjectInputStream tois = new ObjectInputStream(bais);
		    Object value = tois.readObject();
		    hash.put(key, value);
		    tois.close();
                    */

                    Object value = ois.readObject();
		    hash.put(key, value);
		} catch (ClassNotFoundException cnfe) {
		    failed = true;
		} catch (OptionalDataException ode) {
		    failed = true;
		}
	    }
	    ois.close();
            ris.close();
	} catch (IOException ioe) {
	    System.err.println("IOException in readResource: " + ioe);
	    return false;
	} catch (Throwable t) {
	    return false;
	}

        return true;
    }


    /////////////////////////////////////////////////////////
    //
    // Static methods to get/save supported formats
    // to a runtime/static database.  This is to speed up
    // the runtime for TrackControl.getSupportedFormats()
    /////////////////////////////////////////////////////////

    // We have 3 sets of tables, one for audio formats, one for
    // video formats, one for "other" formats.  This will speed
    // up the search a little.
    static FormatTable audioFmtTbl;
    static FormatTable videoFmtTbl;
    static FormatTable miscFmtTbl;

    static Object fmtTblSync = new Object();

    static int AUDIO_TBL_SIZE = 40;
    static int VIDEO_TBL_SIZE = 20;
    static int MISC_TBL_SIZE = 10;

    static String AUDIO_SIZE_KEY = "ATS";
    static String AUDIO_INPUT_KEY = "AI.";
    static String AUDIO_FORMAT_KEY = "AF.";
    static String AUDIO_HIT_KEY = "AH.";
    static String VIDEO_SIZE_KEY = "VTS";
    static String VIDEO_INPUT_KEY = "VI.";
    static String VIDEO_FORMAT_KEY = "VF.";
    static String VIDEO_HIT_KEY = "VH.";
    static String MISC_SIZE_KEY = "MTS";
    static String MISC_INPUT_KEY = "MI.";
    static String MISC_FORMAT_KEY = "MF.";
    static String MISC_HIT_KEY = "MH.";

    static boolean needSaving = false;

    /**
     * Initialize the supported format tables.
     */
    static final void initDB() {
      synchronized (fmtTblSync) {
	audioFmtTbl = new FormatTable(AUDIO_TBL_SIZE);
	videoFmtTbl = new FormatTable(VIDEO_TBL_SIZE);
	miscFmtTbl = new FormatTable(MISC_TBL_SIZE);
	loadDB();
      }
    }


    /**
     * Reset (destroy) the supported format tables.
     */
    public static final void purgeDB() {
      synchronized (fmtTblSync) {
	if (audioFmtTbl == null)
	    return;
	audioFmtTbl = new FormatTable(AUDIO_TBL_SIZE);
	videoFmtTbl = new FormatTable(VIDEO_TBL_SIZE);
	miscFmtTbl = new FormatTable(MISC_TBL_SIZE);
      }
    }


    /**
     * Given an input format, check the tables to for its supported
     * formats. 
     */
    public static final Format[] getDB(Format input) {

      synchronized (fmtTblSync) {

	if (audioFmtTbl == null)
	    initDB();

	if (input instanceof AudioFormat)
	    return audioFmtTbl.get(input);
	else if (input instanceof VideoFormat)
	    return videoFmtTbl.get(input);

	return miscFmtTbl.get(input);
      }
    }


    /**
     * Save an input format and it's supported formats to the table.
     */
    public static final Format [] putDB(Format input, Format supported[]) {

      synchronized (fmtTblSync) {

	Format in = input.relax();
	Format list[] = new Format[supported.length];
	for (int i = 0; i < supported.length; i++)
	    list[i] = supported[i].relax();

	if (in instanceof AudioFormat)
	    audioFmtTbl.save(in, list);
	else if (in instanceof VideoFormat)
	    videoFmtTbl.save(in, list);
	else
	    miscFmtTbl.save(in, list);

	needSaving = true;

	return list;
      }
    }


    /**
     * Load the supported format table from the resource file.
     */
    private static final void loadDB() {

      synchronized (fmtTblSync) {

	int i, size;
	Object key, value, hit;

	key = Resource.get(AUDIO_SIZE_KEY);
	if (key instanceof Integer)
	    size = ((Integer)key).intValue();
	else
	    size = 0;
	if (size > AUDIO_TBL_SIZE) {
	    // Something's wrong.
	    System.err.println("Resource file is corrupted");
	    size = AUDIO_TBL_SIZE;
	}
	//System.err.println("audio table size = " + size);
	audioFmtTbl.last = size;
	for (i = 0; i < size; i++) {
	    key = Resource.get(AUDIO_INPUT_KEY + i);
	    value = Resource.get(AUDIO_FORMAT_KEY + i);
	    hit = Resource.get(AUDIO_HIT_KEY + i);
	    if (key instanceof Format && value instanceof Format[] &&
		hit instanceof Integer) {
		audioFmtTbl.keys[i] = (Format)key;
		audioFmtTbl.table[i] = (Format[])value;
		audioFmtTbl.hits[i] = ((Integer)hit).intValue();
	    } else {
		System.err.println("Resource file is corrupted");
		audioFmtTbl.last = 0;
		break;
	    }
	}

	key = Resource.get(VIDEO_SIZE_KEY);
	if (key instanceof Integer)
	    size = ((Integer)key).intValue();
	else
	    size = 0;
	if (size > VIDEO_TBL_SIZE) {
	    // Something's wrong.
	    System.err.println("Resource file is corrupted");
	    size = VIDEO_TBL_SIZE;
	}
	//System.err.println("video table size = " + size);
	videoFmtTbl.last = size;
	for (i = 0; i < size; i++) {
	    key = Resource.get(VIDEO_INPUT_KEY + i);
	    value = Resource.get(VIDEO_FORMAT_KEY + i);
	    hit = Resource.get(VIDEO_HIT_KEY + i);
	    if (key instanceof Format && value instanceof Format[] &&
		hit instanceof Integer) {
		videoFmtTbl.keys[i] = (Format)key;
		videoFmtTbl.table[i] = (Format[])value;
		videoFmtTbl.hits[i] = ((Integer)hit).intValue();
	    } else {
		System.err.println("Resource file is corrupted");
		videoFmtTbl.last = 0;
		break;
	    }
	}

	key = Resource.get(MISC_SIZE_KEY);
	if (key instanceof Integer)
	    size = ((Integer)key).intValue();
	else
	    size = 0;
	if (size > MISC_TBL_SIZE) {
	    // Something's wrong.
	    System.err.println("Resource file is corrupted");
	    size = MISC_TBL_SIZE;
	}
	//System.err.println("misc table size = " + size);
	miscFmtTbl.last = size;
	for (i = 0; i < size; i++) {
	    key = Resource.get(MISC_INPUT_KEY + i);
	    value = Resource.get(MISC_FORMAT_KEY + i);
	    hit = Resource.get(MISC_HIT_KEY + i);
	    if (key instanceof Format && value instanceof Format[] &&
		hit instanceof Integer) {
		miscFmtTbl.keys[i] = (Format)key;
		miscFmtTbl.table[i] = (Format[])value;
		miscFmtTbl.hits[i] = ((Integer)hit).intValue();
	    } else {
		System.err.println("Resource file is corrupted");
		miscFmtTbl.last = 0;
		break;
	    }
	}

      } // synchronized
    }


    /**
     * Save the table of supported formats to the resource file.
     */
    public static final void saveDB() {

      synchronized (fmtTblSync) {

	if (!needSaving)
	    return;

	Resource.reset();

	int i;
	Resource.set(AUDIO_SIZE_KEY, new Integer(audioFmtTbl.last));
	for (i = 0; i < audioFmtTbl.last; i++) {
	    Resource.set(AUDIO_INPUT_KEY + i, audioFmtTbl.keys[i]);
	    Resource.set(AUDIO_FORMAT_KEY + i, audioFmtTbl.table[i]);
	    Resource.set(AUDIO_HIT_KEY + i, new Integer(audioFmtTbl.hits[i]));
	}
	Resource.set(VIDEO_SIZE_KEY, new Integer(videoFmtTbl.last));
	for (i = 0; i < videoFmtTbl.last; i++) {
	    Resource.set(VIDEO_INPUT_KEY + i, videoFmtTbl.keys[i]);
	    Resource.set(VIDEO_FORMAT_KEY + i, videoFmtTbl.table[i]);
	    Resource.set(VIDEO_HIT_KEY + i, new Integer(videoFmtTbl.hits[i]));
	}
	Resource.set(MISC_SIZE_KEY, new Integer(miscFmtTbl.last));
	for (i = 0; i < miscFmtTbl.last; i++) {
	    Resource.set(MISC_INPUT_KEY + i, miscFmtTbl.keys[i]);
	    Resource.set(MISC_FORMAT_KEY + i, miscFmtTbl.table[i]);
	    Resource.set(MISC_HIT_KEY + i, new Integer(miscFmtTbl.hits[i]));
	}

	try {
	    Resource.commit();
	} catch (Throwable e) {
	    //System.err.println("Cannot save resource file: " + e);
	}

	needSaving = false;

      } // synchronized
    }
}


/**
 * This is a utility class to store supported formats in a table.
 * This cannot be an internal class since it has to be used in
 * static methods.
 */
class FormatTable {

    public Format keys[];
    public Format table[][];
    public int hits[];
    public int last;

    public FormatTable(int size) {
	keys = new Format[size];
	table = new Format[size][];
	hits = new int[size];
	last = 0;
    }

    /**
     * Given an input format, check the tables to for its supported formats. 
     */
    Format[] get(Format input) {
	Format res[] = null;
	for (int i = 0; i < last; i++) {
	    if (res == null && keys[i].matches(input)) {
		res = table[i];
		hits[i] = keys.length;
		//System.err.println("found match");
	    } else
		hits[i] = hits[i] - 1;
	}
	return res;
    }

    /**
     * Save the supported formats with a input key to the table.
     */
    public void save(Format input, Format supported[]) {
	int idx;
	if (last >= keys.length) {
	    idx = findLeastHit();
	} else {
	    idx = last;
	    last++;
	}
	keys[idx] = input;
	table[idx] = supported;
	hits[idx] = keys.length;
    }

    /**
     * Find the least used entry in the table.
     */
    public int findLeastHit() {
	int min = hits[0];
	int idx = 0;
	for (int i = 1; i < last; i++) {
	    if (hits[i] < min) {
		min = hits[i];
		idx = i;
	    }
	}
	return idx;
    }
}


